library(plotly)
library(data.table)
library(tidyr)
library(knitr)
library(heatmaply)
Preprocessing
- Load data file
- rename genres for better readability
- “Religion, Spirituality & New Age” to “Religion”
- “Science.fiction” to “SciFi”
- “Action.and.Adventure” to “Action”
All genres:
books_mat <- read.csv(file_bookstore, row.names = 1)
rownames(books_mat) <- make.names(rownames(books_mat))
rownames(books_mat) <- sub("Science.fiction", "SciFi", rownames(books_mat))
rownames(books_mat) <- sub("Action.and.Adventure", "Action", rownames(books_mat))
rownames(books_mat) <- sub("Religion..Spirituality...New.Age",
"Religion", rownames(books_mat)
)
colnames(books_mat) <- rownames(books_mat)
rownames(books_mat)
[1] "Satire" "SciFi" "Drama" "Action"
[5] "Romance" "Mystery" "Horror" "Self.help"
[9] "Health" "Guide" "Travel" "Children.s"
[13] "Religion" "Science" "History" "Math"
[17] "Anthology" "Poetry" "Encyclopedias" "Dictionaries"
[21] "Comics" "Art" "Cookbooks" "Diaries"
[25] "Journals"
- Check if upper and lower triangle identical
is_upper_lower <- identical(
books_mat[upper.tri(books_mat)],
t(books_mat)[upper.tri(books_mat)]
)
is_upper_lower
[1] TRUE
- Transform to long and tidy
data.table
books_dt <- as.data.table(books_mat, keep.rownames = TRUE)
setnames(books_dt, c('genreA',colnames(books_mat)))
books_dt <- as.data.table(gather(books_dt, genreB, customers, Satire:Journals))
- Average number of genres per customer
sum(books_dt[genreA==genreB, customers])/total_customers
[1] 2.332187
First ideas
Show me everything!
hm <- heatmapr(books_mat)
heatmaply(hm,
plot_method = 'plotly',
colors = c('grey95', 'dodgerblue')
)
- Romance, SciFi, Action, History are most bought
- bought-together clusters:
- Romance, SciFi, Action, History
- Math and Poetry
- Mystery is an outlier
Most bought genre
plot_ly(data=books_dt[genreA==genreB][order(customers)],
x=~genreA, y=~customers, type="bar"
)%>% layout(
margin=list(b=100),
xaxis=list(categoryorder="trace"),
title="Most bought genre"
)
Best pairs
all_genres <- unique(books_dt$genreA)
all_pairs <- combn(all_genres, 2, simplify = F)
pair_customers <-
pair_dt <- data.table(
genre_pairs = sapply(all_pairs, function(p){
paste(sort(p), collapse = "&")}
),
pair_customers = sapply(all_pairs, function(p){
books_dt[genreA==p[1] & genreB==p[2], customers]
})
)
plot_ly(data=pair_dt[order(pair_customers, decreasing=T)][1:10], type='bar',
x=~genre_pairs, y=~pair_customers
)%>% layout(
margin=list(b=100),
xaxis=list(categoryorder="trace"),
title="Top 10 genre pairs"
)
- mostly combinations of most bought genres
Special genres
Hypothesis
- If a customer buys more than 2 genres, he is recorded in more than 1 off-diagonal entry:
- (2*diagonal - colSum) < 0
- If a genre is bought more often alone than in triplets (or higher):
- (2*diagonal - colSum) > 0
Look for customers that buy only one genre
- Compare
column sum and 2*diagonal value
- generate table with
{genre, {2*diagonal-colSum}}
all_genres <- unique(books_dt$genreA)
selective_dt <- data.table()
for(g in all_genres){
d <- books_dt[genreA==g & genreB==g, customers]
cs <- sum(books_dt[genreA==g, customers])
dd <- I(2*d - cs)
selective_dt <- rbind(selective_dt, data.table(genre=g, diag_diff=dd))
}
p_sel <- plot_ly(
data=selective_dt[order(diag_diff)],
y=~genre, x=~diag_diff, type="bar",
color = ~diag_diff>0, colors=c("gray", "darkgreen")
)%>% layout(
margin=list(l=100),
yaxis=list(categoryorder="trace", title=''),
xaxis=list(title='2*diagonal - columnSum'),
title="Which genres are bought alone?"
)
show(p_sel)
- Mystery and Horror are mostly bought alone
- Satire and Travel rather bought in pairs
Normalize columns by diagonal
books_dt[,
rel_customers:= (customers/books_dt[genreA==genreB, customers]),
by=genreB
]
head(books_dt[order(genreA)])
–> genreB relative to genreA-diagonal value
Look at all data unsorted: No pattern.
plot_ly(
data=books_dt, x=~genreA, y=~genreB
) %>%
add_heatmap(
z=~rel_customers, colors= c('grey95', 'dodgerblue')
) %>%
layout(
margin=list(b=110, l=110)
)
With clustering of rows and columns (Note: they are different now):
books_relmat <- dcast(books_dt, genreA ~ genreB, value.var = "rel_customers")
books_relmat <- as.matrix(books_relmat[,genreA:=NULL])
rownames(books_relmat) <- colnames(books_relmat)
hmrel <- heatmapr(t(books_relmat), k_col=3, k_row=3)
heatmaply(
x=hmrel,
plot_method = 'plotly',
colors = c('grey95', 'dodgerblue'),
xlab='genreA', ylab='genreB'
) %>% layout(
title='Customers of genreB relative to genreA',
margin=list(t=50)
)
- 2 hubs on genreA axis (top dendro)
- Art, Journals, Action, SciFi, History
- Encyclopedias, Comics, Disctionaries, Poetry, Math, Anthology
- e.g. genres that were bought with Art were also bought together with Journals
- 2 hubs on genreB axis (right dendro)
- Romance, History, Action, SciFi –> Romance instead of Art and Journals
- same
- bought with everything else? Romance
Most favorite partner genre
plot_ly(
data=books_dt[,median(rel_customers), by=genreB][order(V1, decreasing = T)]
)%>%
add_bars(x=~genreB, y=~V1)%>%
layout(
yaxis=list(title='Median relative customers'),
xaxis=list(categoryorder='trace'),
margin=list(b=100)
)
–> about 20% customers additionally bought SciFi and Romance
Relative best pairs
plot_ly(
data = books_dt[genreA != genreB][order(rel_customers, decreasing = T)][1:10]
) %>%
add_bars(
x=~paste0(genreA, "&", genreB), y=~rel_customers
) %>%
layout(
margin=list(b=100, r=80),
xaxis=list(categoryorder="trace", title=''),
yaxis=list(exponentformat='none'),
title="Top 10 relative genre pairs"
)
–> Math is poetry and History is Science fiction!
LS0tCnRpdGxlOiAiQWxsaWFueiBEYXRhVml6IENoYWxsZW5nZSIKYXV0aG9yOiAiRGFuaWVsIEJhZGVyIgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazoKICAgIHRvYzogeWVzCiAgICB0b2NfZmxvYXQ6IHllcwogIGh0bWxfZG9jdW1lbnQ6CiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB5ZXMKLS0tCgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgZWNobz1UfQpsaWJyYXJ5KHBsb3RseSkKbGlicmFyeShkYXRhLnRhYmxlKQpsaWJyYXJ5KHRpZHlyKQpsaWJyYXJ5KGtuaXRyKQpsaWJyYXJ5KGhlYXRtYXBseSkKYGBgCgoKYGBge3IsIGVjaG89RkFMU0V9Cm9wdHNfY2h1bmskc2V0KGVjaG89VCwgY2FjaGU9RkFMU0UpCnRvdGFsX2N1c3RvbWVycyA8LSAxOTUzODcKZmlsZV9ib29rc3RvcmUgPC0gZmlsZS5wYXRoKCJ+L0Rvd25sb2Fkcy90b3lkYXRhL2Jvb2tfZ2VucmVzX2RhdGEuY3N2IikKc291cmNlKCJidWlsZF9ib29rX3N0b3JlLlIiKQpgYGAKCgojIFByZXByb2Nlc3NpbmcKCiogTG9hZCBkYXRhIGZpbGUKKiByZW5hbWUgZ2VucmVzIGZvciBiZXR0ZXIgcmVhZGFiaWxpdHkKICAgICogIlJlbGlnaW9uLCBTcGlyaXR1YWxpdHkgJiBOZXcgQWdlIiB0byAiUmVsaWdpb24iCiAgICAqICJTY2llbmNlLmZpY3Rpb24iIHRvICJTY2lGaSIKICAgICogIkFjdGlvbi5hbmQuQWR2ZW50dXJlIiB0byAiQWN0aW9uIgogICAgCkFsbCBnZW5yZXM6CmBgYHtyfQpib29rc19tYXQgPC0gcmVhZC5jc3YoZmlsZV9ib29rc3RvcmUsIHJvdy5uYW1lcyA9IDEpCnJvd25hbWVzKGJvb2tzX21hdCkgPC0gbWFrZS5uYW1lcyhyb3duYW1lcyhib29rc19tYXQpKQpyb3duYW1lcyhib29rc19tYXQpIDwtIHN1YigiU2NpZW5jZS5maWN0aW9uIiwgIlNjaUZpIiwgcm93bmFtZXMoYm9va3NfbWF0KSkKcm93bmFtZXMoYm9va3NfbWF0KSA8LSBzdWIoIkFjdGlvbi5hbmQuQWR2ZW50dXJlIiwgIkFjdGlvbiIsIHJvd25hbWVzKGJvb2tzX21hdCkpCnJvd25hbWVzKGJvb2tzX21hdCkgPC0gc3ViKCJSZWxpZ2lvbi4uU3Bpcml0dWFsaXR5Li4uTmV3LkFnZSIsIAogICAgIlJlbGlnaW9uIiwgcm93bmFtZXMoYm9va3NfbWF0KQopCmNvbG5hbWVzKGJvb2tzX21hdCkgPC0gcm93bmFtZXMoYm9va3NfbWF0KQpyb3duYW1lcyhib29rc19tYXQpCmBgYAoKKiBDaGVjayBpZiB1cHBlciBhbmQgbG93ZXIgdHJpYW5nbGUgaWRlbnRpY2FsCgpgYGB7cn0KaXNfdXBwZXJfbG93ZXIgPC0gaWRlbnRpY2FsKAogICAgYm9va3NfbWF0W3VwcGVyLnRyaShib29rc19tYXQpXSwgCiAgICB0KGJvb2tzX21hdClbdXBwZXIudHJpKGJvb2tzX21hdCldCikKaXNfdXBwZXJfbG93ZXIKYGBgCgoqIFRyYW5zZm9ybSB0byBsb25nIGFuZCB0aWR5IGBkYXRhLnRhYmxlYAoKYGBge3J9CmJvb2tzX2R0IDwtIGFzLmRhdGEudGFibGUoYm9va3NfbWF0LCBrZWVwLnJvd25hbWVzID0gVFJVRSkKc2V0bmFtZXMoYm9va3NfZHQsIGMoJ2dlbnJlQScsY29sbmFtZXMoYm9va3NfbWF0KSkpCmJvb2tzX2R0IDwtIGFzLmRhdGEudGFibGUoZ2F0aGVyKGJvb2tzX2R0LCBnZW5yZUIsIGN1c3RvbWVycywgU2F0aXJlOkpvdXJuYWxzKSkKYGBgCgpgYGB7ciwgZWNobz1GfQpoZWFkKGJvb2tzX2R0KQpgYGAKCgoqIEF2ZXJhZ2UgbnVtYmVyIG9mIGdlbnJlcyBwZXIgY3VzdG9tZXIKCmBgYHtyfQpzdW0oYm9va3NfZHRbZ2VucmVBPT1nZW5yZUIsIGN1c3RvbWVyc10pL3RvdGFsX2N1c3RvbWVycwpgYGAKCgojIEZpcnN0IGlkZWFzCgojIyBTaG93IG1lIGV2ZXJ5dGhpbmchCgpgYGB7ciwgZmlnLndpZHRoPTgsIGZpZy5oZWlnaHQ9OH0KaG0gPC0gaGVhdG1hcHIoYm9va3NfbWF0KQpoZWF0bWFwbHkoaG0sIAogICAgcGxvdF9tZXRob2QgPSAncGxvdGx5JywgCiAgICBjb2xvcnMgPSAgYygnZ3JleTk1JywgJ2RvZGdlcmJsdWUnKQopCmBgYAoKKiBSb21hbmNlLCBTY2lGaSwgQWN0aW9uLCBIaXN0b3J5IGFyZSBtb3N0IGJvdWdodCAKKiBib3VnaHQtdG9nZXRoZXIgY2x1c3RlcnM6CiAgICAqIFJvbWFuY2UsIFNjaUZpLCBBY3Rpb24sIEhpc3RvcnkKICAgICogTWF0aCBhbmQgUG9ldHJ5CiogTXlzdGVyeSBpcyBhbiBvdXRsaWVyCgojIyBNb3N0IGJvdWdodCBnZW5yZQoKYGBge3J9CnBsb3RfbHkoZGF0YT1ib29rc19kdFtnZW5yZUE9PWdlbnJlQl1bb3JkZXIoY3VzdG9tZXJzKV0sIAogICAgeD1+Z2VucmVBLCB5PX5jdXN0b21lcnMsIHR5cGU9ImJhciIKKSU+JSBsYXlvdXQoCiAgICBtYXJnaW49bGlzdChiPTEwMCksIAogICAgeGF4aXM9bGlzdChjYXRlZ29yeW9yZGVyPSJ0cmFjZSIpLAogICAgdGl0bGU9Ik1vc3QgYm91Z2h0IGdlbnJlIgopCmBgYAoKIyMgQmVzdCBwYWlycwoKYGBge3J9CmFsbF9nZW5yZXMgPC0gdW5pcXVlKGJvb2tzX2R0JGdlbnJlQSkKYWxsX3BhaXJzIDwtIGNvbWJuKGFsbF9nZW5yZXMsIDIsIHNpbXBsaWZ5ID0gRikKcGFpcl9jdXN0b21lcnMgPC0gCnBhaXJfZHQgPC0gZGF0YS50YWJsZSgKICAgIGdlbnJlX3BhaXJzID0gc2FwcGx5KGFsbF9wYWlycywgZnVuY3Rpb24ocCl7CiAgICAgICAgcGFzdGUoc29ydChwKSwgY29sbGFwc2UgPSAiJiIpfQogICAgKSwKICAgIHBhaXJfY3VzdG9tZXJzID0gc2FwcGx5KGFsbF9wYWlycywgZnVuY3Rpb24ocCl7CiAgICAgICAgYm9va3NfZHRbZ2VucmVBPT1wWzFdICYgZ2VucmVCPT1wWzJdLCBjdXN0b21lcnNdCiAgICB9KQopCnBsb3RfbHkoZGF0YT1wYWlyX2R0W29yZGVyKHBhaXJfY3VzdG9tZXJzLCBkZWNyZWFzaW5nPVQpXVsxOjEwXSwgdHlwZT0nYmFyJywgCiAgICB4PX5nZW5yZV9wYWlycywgeT1+cGFpcl9jdXN0b21lcnMKKSU+JSBsYXlvdXQoCiAgICBtYXJnaW49bGlzdChiPTEwMCksIAogICAgeGF4aXM9bGlzdChjYXRlZ29yeW9yZGVyPSJ0cmFjZSIpLAogICAgdGl0bGU9IlRvcCAxMCBnZW5yZSBwYWlycyIKKQpgYGAKCiogbW9zdGx5IGNvbWJpbmF0aW9ucyBvZiBtb3N0IGJvdWdodCBnZW5yZXMKCgojIFNwZWNpYWwgZ2VucmVzCgpIeXBvdGhlc2lzCgoqIElmIGEgY3VzdG9tZXIgYnV5cyBtb3JlIHRoYW4gMiBnZW5yZXMsIApoZSBpcyByZWNvcmRlZCBpbiBtb3JlIHRoYW4gMSBvZmYtZGlhZ29uYWwgZW50cnk6CiAgICAqICgyKmRpYWdvbmFsIC0gY29sU3VtKSA8IDAKKiBJZiBhIGdlbnJlIGlzIGJvdWdodCBtb3JlIG9mdGVuIGFsb25lIHRoYW4gaW4gdHJpcGxldHMgKG9yIGhpZ2hlcik6IAogICAgKiAoMipkaWFnb25hbCAtIGNvbFN1bSkgPiAwCgoKTG9vayBmb3IgY3VzdG9tZXJzIHRoYXQgYnV5IG9ubHkgb25lIGdlbnJlCgoqIENvbXBhcmUgYGNvbHVtbiBzdW1gIGFuZCAgYDIqZGlhZ29uYWwgdmFsdWVgCiogZ2VuZXJhdGUgdGFibGUgd2l0aCBge2dlbnJlLCB7MipkaWFnb25hbC1jb2xTdW19fWAKCgpgYGB7ciwgd2FybmluZz1GQUxTRSwgZmlnLmhlaWdodD01fQphbGxfZ2VucmVzIDwtIHVuaXF1ZShib29rc19kdCRnZW5yZUEpCnNlbGVjdGl2ZV9kdCA8LSBkYXRhLnRhYmxlKCkKZm9yKGcgaW4gYWxsX2dlbnJlcyl7CiAgICBkIDwtIGJvb2tzX2R0W2dlbnJlQT09ZyAmIGdlbnJlQj09ZywgY3VzdG9tZXJzXQogICAgY3MgPC0gc3VtKGJvb2tzX2R0W2dlbnJlQT09ZywgY3VzdG9tZXJzXSkKICAgIGRkIDwtIEkoMipkIC0gY3MpCiAgICBzZWxlY3RpdmVfZHQgPC0gcmJpbmQoc2VsZWN0aXZlX2R0LCBkYXRhLnRhYmxlKGdlbnJlPWcsIGRpYWdfZGlmZj1kZCkpCn0KCnBfc2VsIDwtIHBsb3RfbHkoCiAgICBkYXRhPXNlbGVjdGl2ZV9kdFtvcmRlcihkaWFnX2RpZmYpXSwgCiAgICB5PX5nZW5yZSwgeD1+ZGlhZ19kaWZmLCB0eXBlPSJiYXIiLCAKICAgIGNvbG9yID0gfmRpYWdfZGlmZj4wLCBjb2xvcnM9YygiZ3JheSIsICJkYXJrZ3JlZW4iKQopJT4lIGxheW91dCgKICAgIG1hcmdpbj1saXN0KGw9MTAwKSwgCiAgICB5YXhpcz1saXN0KGNhdGVnb3J5b3JkZXI9InRyYWNlIiwgdGl0bGU9JycpLAogICAgeGF4aXM9bGlzdCh0aXRsZT0nMipkaWFnb25hbCAtIGNvbHVtblN1bScpLAogICAgdGl0bGU9IldoaWNoIGdlbnJlcyBhcmUgYm91Z2h0IGFsb25lPyIKKQoKc2hvdyhwX3NlbCkKYGBgCiogTXlzdGVyeSBhbmQgSG9ycm9yIGFyZSBtb3N0bHkgYm91Z2h0IGFsb25lCiogU2F0aXJlIGFuZCBUcmF2ZWwgcmF0aGVyIGJvdWdodCBpbiBwYWlycwoKCgojIE5vcm1hbGl6ZSBjb2x1bW5zIGJ5IGRpYWdvbmFsCgpgYGB7cn0KYm9va3NfZHRbLAogICAgcmVsX2N1c3RvbWVyczo9IChjdXN0b21lcnMvYm9va3NfZHRbZ2VucmVBPT1nZW5yZUIsIGN1c3RvbWVyc10pLCAKICAgIGJ5PWdlbnJlQgogICAgXQpoZWFkKGJvb2tzX2R0W29yZGVyKGdlbnJlQSldKQpgYGAKCi0tPiBnZW5yZUIgcmVsYXRpdmUgdG8gZ2VucmVBLWRpYWdvbmFsIHZhbHVlIAoKTG9vayBhdCBhbGwgZGF0YSB1bnNvcnRlZDogTm8gcGF0dGVybi4KCmBgYHtyLCBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD04fQpwbG90X2x5KAogICAgZGF0YT1ib29rc19kdCwgeD1+Z2VucmVBLCB5PX5nZW5yZUIKICAgICkgJT4lCiAgICBhZGRfaGVhdG1hcCgKICAgICAgICB6PX5yZWxfY3VzdG9tZXJzLCBjb2xvcnM9IGMoJ2dyZXk5NScsICdkb2RnZXJibHVlJykKICAgICkgJT4lCiAgICBsYXlvdXQoCiAgICAgICAgbWFyZ2luPWxpc3QoYj0xMTAsIGw9MTEwKQogICAgKQpgYGAKCldpdGggY2x1c3RlcmluZyBvZiByb3dzIGFuZCBjb2x1bW5zIChOb3RlOiB0aGV5IGFyZSBkaWZmZXJlbnQgbm93KToKCmBgYHtyLCBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD04fQpib29rc19yZWxtYXQgPC0gZGNhc3QoYm9va3NfZHQsIGdlbnJlQSB+IGdlbnJlQiwgdmFsdWUudmFyID0gInJlbF9jdXN0b21lcnMiKQpib29rc19yZWxtYXQgPC0gYXMubWF0cml4KGJvb2tzX3JlbG1hdFssZ2VucmVBOj1OVUxMXSkKcm93bmFtZXMoYm9va3NfcmVsbWF0KSA8LSBjb2xuYW1lcyhib29rc19yZWxtYXQpCgpobXJlbCA8LSBoZWF0bWFwcih0KGJvb2tzX3JlbG1hdCksIGtfY29sPTMsIGtfcm93PTMpCmhlYXRtYXBseSgKICAgIHg9aG1yZWwsIAogICAgcGxvdF9tZXRob2QgPSAncGxvdGx5JywgCiAgICBjb2xvcnMgPSAgYygnZ3JleTk1JywgJ2RvZGdlcmJsdWUnKSwKICAgIHhsYWI9J2dlbnJlQScsIHlsYWI9J2dlbnJlQicKKSAlPiUgbGF5b3V0KAogICAgdGl0bGU9J0N1c3RvbWVycyBvZiBnZW5yZUIgcmVsYXRpdmUgdG8gZ2VucmVBJywKICAgIG1hcmdpbj1saXN0KHQ9NTApCikKYGBgCgoqIDIgaHVicyBvbiBnZW5yZUEgYXhpcyAodG9wIGRlbmRybykKICAgICogQXJ0LCBKb3VybmFscywgQWN0aW9uLCBTY2lGaSwgSGlzdG9yeQogICAgKiBFbmN5Y2xvcGVkaWFzLCBDb21pY3MsIERpc2N0aW9uYXJpZXMsIFBvZXRyeSwgTWF0aCwgQW50aG9sb2d5CiAgICAqIGUuZy4gZ2VucmVzIHRoYXQgd2VyZSBib3VnaHQgd2l0aCBBcnQgd2VyZSBhbHNvIGJvdWdodCB0b2dldGhlciB3aXRoIEpvdXJuYWxzCiogMiBodWJzIG9uIGdlbnJlQiBheGlzIChyaWdodCBkZW5kcm8pCiAgICAqIFJvbWFuY2UsIEhpc3RvcnksIEFjdGlvbiwgU2NpRmkgLS0+IFJvbWFuY2UgaW5zdGVhZCBvZiBBcnQgYW5kIEpvdXJuYWxzCiAgICAqIHNhbWUKKiBib3VnaHQgd2l0aCBldmVyeXRoaW5nIGVsc2U/IFJvbWFuY2UKCgojIyBNb3N0IGZhdm9yaXRlIHBhcnRuZXIgZ2VucmUKCmBgYHtyfQpwbG90X2x5KAogICAgZGF0YT1ib29rc19kdFssbWVkaWFuKHJlbF9jdXN0b21lcnMpLCBieT1nZW5yZUJdW29yZGVyKFYxLCBkZWNyZWFzaW5nID0gVCldCiAgICApJT4lCiAgICBhZGRfYmFycyh4PX5nZW5yZUIsIHk9flYxKSU+JQogICAgbGF5b3V0KAogICAgICAgIHlheGlzPWxpc3QodGl0bGU9J01lZGlhbiByZWxhdGl2ZSBjdXN0b21lcnMnKSwKICAgICAgICB4YXhpcz1saXN0KGNhdGVnb3J5b3JkZXI9J3RyYWNlJyksCiAgICAgICAgbWFyZ2luPWxpc3QoYj0xMDApCiAgICApCmBgYAoKLS0+IGFib3V0IDIwJSBjdXN0b21lcnMgYWRkaXRpb25hbGx5IGJvdWdodCBTY2lGaSBhbmQgUm9tYW5jZQoKCiMjIFJlbGF0aXZlIGJlc3QgcGFpcnMKCmBgYHtyfQpwbG90X2x5KAogICAgICAgIGRhdGEgPSBib29rc19kdFtnZW5yZUEgIT0gZ2VucmVCXVtvcmRlcihyZWxfY3VzdG9tZXJzLCBkZWNyZWFzaW5nID0gVCldWzE6MTBdCiAgICApICU+JSAKICAgIGFkZF9iYXJzKAogICAgICAgIHg9fnBhc3RlMChnZW5yZUEsICImIiwgZ2VucmVCKSwgeT1+cmVsX2N1c3RvbWVycwogICAgICAgICkgJT4lIAogICAgbGF5b3V0KAogICAgICAgIG1hcmdpbj1saXN0KGI9MTAwLCByPTgwKSwgCiAgICAgICAgeGF4aXM9bGlzdChjYXRlZ29yeW9yZGVyPSJ0cmFjZSIsIHRpdGxlPScnKSwKICAgICAgICB5YXhpcz1saXN0KGV4cG9uZW50Zm9ybWF0PSdub25lJyksCiAgICAgICAgdGl0bGU9IlRvcCAxMCByZWxhdGl2ZSBnZW5yZSBwYWlycyIKKQpgYGAKCi0tPiAqKk1hdGggaXMgcG9ldHJ5IGFuZCBIaXN0b3J5IGlzIFNjaWVuY2UgZmljdGlvbiEqKgo=